##  Copyright (C) 2009 by Johann Duscher (alias Jonny Dee)
##
##  This file is part of PyoRhythm.
##
##  PyoRhythm is free software: you can redistribute it and/or modify
##  it under the terms of the GNU General Public License as published by
##  the Free Software Foundation, either version 3 of the License, or
##  (at your option) any later version.
##
##  PyoRhythm is distributed in the hope that it will be useful,
##  but WITHOUT ANY WARRANTY; without even the implied warranty of
##  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
##  GNU General Public License for more details.
##
##  You should have received a copy of the GNU General Public License
##  along with PyoRhythm.  If not, see <http://www.gnu.org/licenses/>.

from __future__ import division
import math
from datetime import datetime, timedelta, time

from PyQt4 import QtCore
from PyQt4 import QtGui

from pyorhythm.util import subtractTimes
from pyorhythm.model import BioRhythm
from pyorhythm.Ui_MainWindow import Ui_MainWindow


class QBioRhythmWindow(QtGui.QMainWindow):
    def __init__(self, parent=None):
        QtGui.QMainWindow.__init__(self, parent)

        self.mainWindowUi = Ui_MainWindow()
        self.mainWindowUi.setupUi(self)

        self.bioRhythm = QBioRhythm(self.mainWindowUi.bioRhythmGraphWidget)
        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.bioRhythm.sizePolicy().hasHeightForWidth())
        self.bioRhythm.setSizePolicy(sizePolicy)
        self.mainWindowUi.verticalLayout_3.addWidget(self.bioRhythm)
        self.bioRhythm.setShowDayGrid(True)

        self.connect(self.mainWindowUi.birthTimeEdit, QtCore.SIGNAL("dateTimeChanged(QDateTime)"), self.birthTimeChanged)
        self.connect(self.mainWindowUi.startDateEdit, QtCore.SIGNAL("dateChanged(QDate)"), self.startDateChanged)
        self.connect(self.mainWindowUi.endDateEdit, QtCore.SIGNAL("dateChanged(QDate)"), self.endDateChanged)

        self.connect(self, QtCore.SIGNAL("birthTimeChanged"), self.mainWindowUi.birthTimeEdit.setDateTime)
        self.connect(self, QtCore.SIGNAL("startDateChanged"), self.mainWindowUi.startDateEdit.setDate)
        self.connect(self, QtCore.SIGNAL("endDateChanged"), self.mainWindowUi.endDateEdit.setDate)

        self.connect(self.mainWindowUi.averageCycleCheckBox, QtCore.SIGNAL("stateChanged(int)"), self.showAverageCycleChanged)
        self.connect(self.mainWindowUi.emotionalCycleCheckBox, QtCore.SIGNAL("stateChanged(int)"), self.showEmotionalCycleChanged)
        self.connect(self.mainWindowUi.intellectualCycleCheckBox, QtCore.SIGNAL("stateChanged(int)"), self.showIntellectualCycleChanged)
        self.connect(self.mainWindowUi.intuitiveCycleCheckBox, QtCore.SIGNAL("stateChanged(int)"), self.showIntuitiveCycleChanged)
        self.connect(self.mainWindowUi.physicalCycleCheckBox, QtCore.SIGNAL("stateChanged(int)"), self.showPhysicalCycleChanged)

        self.connect(self, QtCore.SIGNAL("showAverageCycleChanged"), self.mainWindowUi.averageCycleCheckBox.setCheckState)
        self.connect(self, QtCore.SIGNAL("showEmotionalCycleChanged"), self.mainWindowUi.emotionalCycleCheckBox.setCheckState)
        self.connect(self, QtCore.SIGNAL("showIntellectualCycleChanged"), self.mainWindowUi.intellectualCycleCheckBox.setCheckState)
        self.connect(self, QtCore.SIGNAL("showIntuitiveCycleChanged"), self.mainWindowUi.intuitiveCycleCheckBox.setCheckState)
        self.connect(self, QtCore.SIGNAL("showPhysicalCycleChanged"), self.mainWindowUi.physicalCycleCheckBox.setCheckState)

        self.connect(self.mainWindowUi.resolutionSlider, QtCore.SIGNAL("valueChanged(int)"), self.resolutionChanged)

        self.connect(self, QtCore.SIGNAL("resolutionChanged"), self.mainWindowUi.resolutionSlider.setValue)


    def birthTimeChanged(self, time):
        birthTime = self.mainWindowUi.birthTimeEdit.dateTime()
        self.setBirthTime(birthTime)


    def startDateChanged(self, date):
        startDate = self.mainWindowUi.startDateEdit.date()
        self.setStartDate(startDate)


    def endDateChanged(self, date):
        endDate = self.mainWindowUi.endDateEdit.date()
        self.setEndDate(endDate)


    def showAverageCycleChanged(self, state):
        flag = self.mainWindowUi.averageCycleCheckBox.checkState() == QtCore.Qt.Checked
        self.setShowCycle(BioRhythm.AverageCycle, flag)


    def showEmotionalCycleChanged(self, state):
        flag = self.mainWindowUi.emotionalCycleCheckBox.checkState() == QtCore.Qt.Checked
        self.setShowCycle(BioRhythm.EmotionalCycle, flag)


    def showIntellectualCycleChanged(self, state):
        flag = self.mainWindowUi.intellectualCycleCheckBox.checkState() == QtCore.Qt.Checked
        self.setShowCycle(BioRhythm.IntellectualCycle, flag)


    def showIntuitiveCycleChanged(self, state):
        flag = self.mainWindowUi.intuitiveCycleCheckBox.checkState() == QtCore.Qt.Checked
        self.setShowCycle(BioRhythm.IntuitiveCycle, flag)


    def showPhysicalCycleChanged(self, state):
        flag = self.mainWindowUi.physicalCycleCheckBox.checkState() == QtCore.Qt.Checked
        self.setShowCycle(BioRhythm.PhysicalCycle, flag)


    def resolutionChanged(self, resolution):
        resolution = float(self.mainWindowUi.resolutionSlider.value())/float(self.mainWindowUi.resolutionSlider.maximum())
        self.setResolution(resolution)


    def setBirthTime(self, birthTime):
        pyBirthTime = birthTime.toPyDateTime()
        # Assume there is exactly one person.
        person = self.bioRhythm.getPersons()[0]
        person.setBirthTime(pyBirthTime)
        self.emit(QtCore.SIGNAL('birthTimeChanged'), birthTime)


    def setShowCycle(self, cycle, flag):
        # Assume there is exactly one person.
        person = self.bioRhythm.getPersons()[0]
        person.setShowCycle(cycle, flag)

        checkState = QtCore.Qt.Checked if flag else QtCore.Qt.Unchecked
        signal = {
            BioRhythm.AverageCycle :'showAverageCycleChanged',
            BioRhythm.EmotionalCycle : 'showEmotionalCycleChanged',
            BioRhythm.IntellectualCycle : 'showIntellectualCycleChanged',
            BioRhythm.IntuitiveCycle : 'showIntuitiveCycleChanged',
            BioRhythm.PhysicalCycle : 'showPhysicalCycleChanged',
        }[cycle]
        self.emit(QtCore.SIGNAL(signal), checkState)


    def setStartDate(self, startDate):
        startTime = datetime.combine(startDate.toPyDate(), time(0, 0))
        self.bioRhythm.setStartTime(startTime)
        self.emit(QtCore.SIGNAL('startDateChanged'), startDate)


    def setEndDate(self, endDate):
        endTime = datetime.combine(endDate.toPyDate(), time(0, 0))
        self.bioRhythm.setEndTime(endTime)
        self.emit(QtCore.SIGNAL('endDateChanged'), endDate)


    def setResolution(self, resolution):
        self.bioRhythm.setResolution(resolution)
        sliderValue = int(resolution * self.mainWindowUi.resolutionSlider.maximum())
        self.emit(QtCore.SIGNAL('resolutionChanged'), sliderValue)


    def addPerson(self, person):
        self.bioRhythm.addPerson(person)


class QBioRhythm(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)

        self.setStartTime(datetime.today())
        self.setEndTime(self._startTime + timedelta(days=31))

        self.setResolution(0.75)

        self.setShowAxis(True)
        self.setShowHalfAxis(True)
        self.setShowBorder(True)
        self.setShowDayGrid(True)
        self.setShowMonthGrid(True)
        self.setShowYearGrid(True)

        self.setAxisPen(QtGui.QPen(QtGui.QColor(0, 0, 0), 5, QtCore.Qt.SolidLine))
        self.setHalfAxisPen(QtGui.QPen(QtGui.QColor(128, 128, 128), 3, QtCore.Qt.DashDotLine))
        self.setBorderPen(QtGui.QPen(QtGui.QColor(0, 0, 0), 5, QtCore.Qt.SolidLine))
        self.setDayGridPen(QtGui.QPen(QtGui.QColor(128, 128, 128), 1, QtCore.Qt.DashLine))
        self.setMonthGridPen(QtGui.QPen(QtGui.QColor(0, 0, 0), 2, QtCore.Qt.SolidLine))
        self.setYearGridPen(QtGui.QPen(QtGui.QColor(0, 0, 0), 5, QtCore.Qt.SolidLine))


    def addPerson(self, person):
        person.setParent(self)
        self.connect(person, QtCore.SIGNAL("personChanged"), self.personChanged)


    def personChanged(self, person):
        self.repaint()


    def setStartTime(self, startTime):
        self._startTime = startTime
        self.repaint()


    def getStartTime(self):
        return self._startTime


    def setEndTime(self, endTime):
        self._endTime = endTime
        self.repaint()


    def getEndTime(self):
        return self._endTime


    def setResolution(self, resolution):
        self._resolution = resolution
        self.repaint()


    def getResolution(self):
        return self._resolution


    def setShowAxis(self, flag):
        self._showAxis = flag
        self.repaint()


    def isShowAxis(self):
        return self._showAxis


    def setShowHalfAxis(self, flag):
        self._showHalfAxis = flag
        self.repaint()


    def isShowHalfAxis(self):
        return self._showHalfAxis


    def setShowBorder(self, flag):
        self._showBorder = flag
        self.repaint()


    def isShowBorder(self):
        return self._showBorder


    def setShowDayGrid(self, flag):
        self._showDayGrid = flag
        self.repaint()


    def isShowDayGrid(self):
        return self._showDayGrid


    def setShowMonthGrid(self, flag):
        self._showMonthGrid = flag
        self.repaint()


    def isShowMonthGrid(self):
        return self._showMonthGrid


    def setShowYearGrid(self, flag):
        self._showYearGrid = flag
        self.repaint()


    def isShowYearGrid(self):
        return self._showYearGrid


    def setAxisPen(self, pen):
        self._axisPen = pen
        self.repaint()


    def getAxisPen(self):
        return self._axisPen


    def setHalfAxisPen(self, pen):
        self._halfAxisPen = pen
        self.repaint()


    def getHalfAxisPen(self):
        return self._halfAxisPen


    def setBorderPen(self, pen):
        self._borderPen = pen
        self.repaint()


    def getBorderPen(self):
        return self._borderPen


    def setDayGridPen(self, pen):
        self._dayGridPen = pen
        self.repaint()


    def getDayGridPen(self):
        return self._dayGridPen


    def setMonthGridPen(self, pen):
        self._monthGridPen = pen
        self.repaint()


    def getMonthGridPen(self):
        return self._monthGridPen


    def setYearGridPen(self, pen):
        self._yearGridPen = pen
        self.repaint()


    def getYearGridPen(self):
        return self._yearGridPen


    def getPersons(self):
        return self.children()


    def paintEvent(self, event):
        paint = QtGui.QPainter()
        paint.begin(self)

        size = self.size()
        width = size.width()
        height = size.height()

        self._paintPersons(paint, width, height)

        if self.isShowDayGrid():
            self._paintDayGrid(paint, width, height)

        if self.isShowMonthGrid():
            self._paintMonthGrid(paint, width, height)

        if self.isShowYearGrid():
            self._paintYearGrid(paint, width, height)

        if self.isShowAxis():
            self._paintAxis(paint, width, height)

        if self.isShowHalfAxis():
            self._paintHalfAxis(paint, width, height)

        if self.isShowBorder():
            self._paintBorder(paint, width, height)

        paint.end()


    def _paintAxis(self, paint, width, height):
        paint.setPen(self._axisPen)
        paint.drawLine(0, height//2-1, width-1, height//2-1)


    def _paintHalfAxis(self, paint, width, height):
        paint.setPen(self._halfAxisPen)
        paint.drawLine(0, height//4-1, width-1, height//4-1)
        paint.drawLine(0, height*3//4-1, width-1, height*3//4-1)


    def _paintBorder(self, paint, width, height):
        paint.setPen(self._borderPen)
        paint.drawRect(0, 0, width-1, height-1)


    def _paintDayGrid(self, paint, width, height):
        font = QtGui.QFont('Serif', 7, QtGui.QFont.Light)
        paint.setFont(font)
        metrics = paint.fontMetrics()
        paint.setPen(self._dayGridPen)
        days = (self._endTime - self._startTime).days
        oneDay = timedelta(days=1)
        time = self._startTime
        for day in range(days):
            x = width * day // days
            paint.drawLine(x, 0, x, height-1)
            # Write lables
            dayStr = str(time.day)
            #fw = metrics.width(dayStr)
            fh = metrics.height()
            paint.drawText(x+3, height//2+fh, dayStr)
            time = time + oneDay


    def _paintMonthGrid(self, paint, width, height):
        font = QtGui.QFont('Serif', 7, QtGui.QFont.Light)
        paint.setFont(font)
        metrics = paint.fontMetrics()
        paint.setPen(self._monthGridPen)
        days = (self._endTime - self._startTime).days
        oneDay = timedelta(days=1)
        time = self._startTime
        for day in range(days):
            if time.day == 1:
                x = width * day // days
                paint.drawLine(x, 0, x, height-1)
                # Write lables
                monthStr = time.strftime('%b')
                #fw = metrics.width(yearStr)
                fh = metrics.height()
                paint.drawText(x+10, height//2-fh, monthStr)
            time = time + oneDay


    def _paintYearGrid(self, paint, width, height):
        font = QtGui.QFont('Serif', 7, QtGui.QFont.Light)
        paint.setFont(font)
        metrics = paint.fontMetrics()
        paint.setPen(self._yearGridPen)
        days = (self._endTime - self._startTime).days
        oneDay = timedelta(days=1)
        time = self._startTime
        for day in range(days):
            if time.day == 1 and time.month == 1:
                x = width * day // days
                paint.drawLine(x, 0, x, height-1)
                # Write lables
                yearStr = str(time.year)
                #fw = metrics.width(yearStr)
                fh = metrics.height()
                paint.drawText(x+10, fh, yearStr)

            time = time + oneDay


    def _mapXToTime(self, x, width):
        days = subtractTimes(self._endTime, self._startTime)
        percentDaySecs, dayx = math.modf(days * x / width)
        delta = timedelta(days=int(dayx), seconds=int(percentDaySecs*3600.0*24.0))
        return self._startTime + delta


    def _mapTimeToY(self, cycle, person, time, height):
        bioValue = BioRhythm.calcBioValue(cycle, person, time)
        return int(height/2.0 - height/2.0*bioValue)


    def _calcStepFromResolution(self, width):
        days = subtractTimes(self._endTime, self._startTime)
        cycleLength = max(BioRhythm.EmotionalCycle, BioRhythm.IntellectualCycle, BioRhythm.IntuitiveCycle, BioRhythm.PhysicalCycle)
        cycleCount = days/cycleLength
        frequency = cycleCount/width
        samplingFrequency = 6.0*frequency
        step = (1.0-self._resolution)*(width/(samplingFrequency*width))+self._resolution
        return int(math.ceil(step))


    def _paintBioCurve(self, paint, person, cycle, width, height):
        bioValueY = lambda x : self._mapTimeToY(cycle, person, self._mapXToTime(x, width), height)

        paint.setPen(person.getCyclePen(cycle))

        step = self._calcStepFromResolution(width)
        xOld, yOld = 0, bioValueY(0)
        for x in range(1, width, step):
            y = bioValueY(x)
            paint.drawLine(xOld, yOld, x, y)
            xOld, yOld = x, y
        x = width - 1
        if xOld < x:
            y = bioValueY(x)
            paint.drawLine(xOld, yOld, x, y)


    def _paintPersons(self, paint, width, height):
        persons = self.getPersons()
        for person in persons:
            for cycle in [BioRhythm.EmotionalCycle,  BioRhythm.IntellectualCycle, BioRhythm.IntuitiveCycle, BioRhythm.PhysicalCycle, BioRhythm.AverageCycle]:
                if person.isShowCycle(cycle):
                    self._paintBioCurve(paint, person, cycle, width, height)
